Tensorflow专题4:迁移学习(transfer learning)和微调fine-tune的区别及迁移学习代码实现

一:区别

1:迁移学习是将已经学习到的知识应用到其他领域,比如通用的语音模型迁移到某个人的语音模型上。

迁移学习就是将一个问题上训练好的模型通过简单的调整使其适用于一个新的问题。

例如利用ImageNet数据集上训练好的Inception-V3模型来解决一个新的图像分类问题,可以保留训练好的Inception-v3模型中所有卷积层的参数,只是替换最后一层全连接层,在最后这一层全连接层之前的网络层称为瓶颈层。而将新的图像通过训练好的卷积神经网络直到瓶颈层的过程可以看成是对图像进行特征提取的过程,瓶颈层输出再通过一个单层的全连接层神经网络可以很好的区分类别,所以有理由相信将瓶颈层的输出的节点向量可以被称为任何图像的更加精简且表达能力更强的特征向量。所以可以直接利用这个训练好的神经网络对图像进行特征提取,然后再将提取得到特征向量作为输入来训练一个新的单层全连接网络来处理分类问题。

但是在数据量足够的情况下,迁移学习的效果不如完全重新训练,但是迁移学习所需要的训练时间和训练样本要远远小于训练完整的模型。

​ 比如把已经训练好的模型的某一层的输出拿出来,然后用一个svm、LR等分类,更好的去利用从某一层输出的特征(也叫知识),这也还是迁移学习的思想,如下前三个是transfer learning经常用到的方法。最后一个是finetune的思想。

把Alexnet里卷积层最后一层输出的特征拿出来,然后直接用SVM分类。这是Transfer Learning,因为你用到了Alexnet中已经学到了的 “知识”。把Vggnet卷积层最后的输出拿出来,用贝叶斯分类器分类。思想基本同上。甚至你可以把Alexnet、Vggnet的输出拿出来进行组合,自己设计一个分类器分类。这个过程中你不仅用了Alexnet的“知识”,也用了Vggnet的“知识”。https://github.com/Gogul09/flower-recognition(此方法实现代码)最后,你也可以直接使用 fine-tune这种方法,在Alexnet的基础上,重新加上全连接层,再去训练网络。

2:finetune(微调):例子:在Alexnet的基础上,我们重新加上一个层再去训练网络,比如再加入一个全连接层,那就是先

固定前面的层,让新加的fc层的loss值降低到一个很低的值,再调低学习率,放开所有层一块去训练这样可以收敛到一个不错的效果。

3:所以我个人认为迁移学习直接将现有的或者从现有的模型中提取出来的有用的东西应用的另一个领域,不在进行训练之前的网络部分,只需要训练我们添加部分网络的部分,将迁移过来的模型的某一层的输出作为我们新增加网络部分的输入。

而finetune就是微调,思想是:利用原有模型的参数信息,作为我们要训练的新的模型的初始化参数,这个新的模型可以和原来一样也可以增添几个层(进行适当的调整)。

4:传统的机器学习框架下,学习的任务是在给定充分训练数据集的基础上学习一个分类模型;然后利用这个学习到的模型来对测试文档进行分类和预测。然而,我们看到机器学习算法在当前web挖掘应用领域存在一个关键问题:一些新出现的领域中的大量训练数据非常难得到。web领域中大量新的数据不断涌现,从传统的新闻,网页,到图片,再到博客,播客等。传统的机器学习需要对每个领域都标定大量训练数据,这将会耗费大量的人力物力,而没有大量的标注数据,会使得很多与学习相关研究与应用无法开展,其次传统的机器学习假设训练数据与测试数据服从相同的数据分布。然而在很多情况下,这种相同分布不满足,通常可能发生的情况如训练数据过期。如果我们有大量的,在不同分布下的训练数据,完全丢弃这些数据也是非常浪费的,如何利用这些数据就是迁移学习主要解决的问题。

迁移学习可以从现有的数据中迁移知识,用来帮助将来的学习。

迁移学习的目标是将从一个环境中学到的知识用来帮助新环境中的学习任务,因此迁移学习不会想传统机器学习那样作同分布假设。

例子:一个会下象棋的人可以更容易的学会下围棋。

迁移学习目前分为一下三个部分:同构空间下基于实例的迁移学习;同构空间下基于特征的迁移学习;异构空间下的迁移学习。

基于实例的迁移学习有更强的知识迁移能力,基于特征的迁移学习具有更广规范的知识迁移能力;异构空间的迁移具有广泛的学习与扩展能力。

迁移学习即一种学习对另一种学习的影响,它广泛的存在于知识技能态度和行为的规范的学习中,任何一种学习都将受先验知识的影响,只要有学习就有迁移,迁移是学习的继续和巩固,优势提高和深化学习的条件,学习与迁移不可分割。

二:迁移学习实例

为了能够快速地训练好自己的花朵图片分类器,我们可以使用别人已经训练好的模型参数,在此基础之上训练我们的模型。这个便属于迁移学习。本文提供训练数据集和代码下载。

原理:卷积神经网络模型总体上可以分为两部分,前面的卷积层和后面的全连接层。卷积层的作用是图片特征的提取,全连接层作用是特征的分类。我们的思路便是在inception-v3网络模型上,修改全连接层,保留卷积层。卷积层的参数使用的是别人已经训练好的,全连接层的参数需要我们初始化并使用我们自己的数据来训练和学习。

和微调fine-tune的区别及迁移学习代码实现/2018-07-15-19-26-42.png)

上面inception-v3模型图红色箭头前面部分是卷积层,后面是全连接层。我们需要修改修改全连接层,同时把模型的最终输出改为5。

由于这里使用了tensorflow框架,所以,我们需要获取上图红色箭头所在位置的张量BOTTLENECK_TENSOR_NAME(最后一个卷积层激活函数的输出值,个数为2048)以及模型最开始的输入数据的张量JPEG_DATA_TENSOR_NAME。获取这两个张量的作用是,图片训练数据通过JPEG_DATA_TENSOR_NAME张量输入模型,通过BOTTLENECK_TENSOR_NAME张量获取通过卷积层之后的图片特征。

BOTTLENECK_TENSOR_SIZE = 2048

BOTTLENECK_TENSOR_NAME = ‘pool_3/_reshape:0’

JPEG_DATA_TENSOR_NAME = ‘DecodeJpeg/contents:0’

通过下面的代码加载模型,同时获取上面所述的两个张量。

和微调fine-tune的区别及迁移学习代码实现/2018-07-15-19-27-04.png)

最后便是定义交叉熵损失函数。模型使用反向传播训练,而训练的参数并不是模型的所有参数,仅仅是全连接层的参数,卷积层的参数是不变的。

定义交叉熵损失函数。

cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=ground_truth_input)

cross_entropy_mean = tf.reduce_mean(cross_entropy)

train_step = tf.train.GradientDescentOptimizer(LEARNING_RATE).minimize(cross_entropy_mean)

那么接下来的是如何给我们的模型输入数据了,这里提供了几个操作数据的函数。由于训练数据集比较小,

先把所有的图片通过JPEG_DATA_TENSOR_NAME张量输入模型,然后获取BOTTLENECK_TENSOR_NAME张量的值并保存到硬盘中。

在模型训练的时候,从硬盘中读取所保存的BOTTLENECK_TENSOR_NAME张量的值作为全连接层的输入数据。因为一张图片可能会被使用多次。

和微调fine-tune的区别及迁移学习代码实现/2018-07-15-19-27-25.png)

运行代码在到时候再去看我的pycharm中的trasform这个项目。

这个代码实现部分参考的是https://blog.csdn.net/liangyihuai/article/details/79219457这个博客的内容

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# coding=utf8

import glob
import os.path
import random
import numpy as np
import tensorflow as tf
from tensorflow.python.platform import gfile

BOTTLENECK_TENSOR_SIZE = 2048#最后一个卷积层激活函数输出值,个数是2048个1×1的
BOTTLENECK_TENSOR_NAME = 'pool_3/_reshape:0'#张量获取通过卷积层之后的图片特征
JPEG_DATA_TENSOR_NAME = 'DecodeJpeg/contents:0'#模型开始的输入数据的张量,张量输入


MODEL_DIR = './inception_dec_2015'#inception模型的闻之
MODEL_FILE= 'tensorflow_inception_graph.pb'#模型文件的准确位置

CACHE_DIR = './bottleneck'#最后一个卷积层输出的每一类的特征,这个要输入到fc中然后进行分类用的。
INPUT_DATA = './flower_photos'

VALIDATION_PERCENTAGE = 10#
TEST_PERCENTAGE = 10#验证集测试集都占10%

LEARNING_RATE = 0.01
STEPS = 4000
BATCH = 100


def create_image_lists(testing_percentage, validation_percentage):
result = {}
sub_dirs = [x[0] for x in os.walk(INPUT_DATA)]#os.walk()方法的作用是在目录树中游走输出在目录中的文件名,向上或者向下。
is_root_dir = True
for sub_dir in sub_dirs:
if is_root_dir:
is_root_dir = False
continue

extensions = ['jpg', 'jpeg', 'JPG', 'JPEG']

file_list = []
dir_name = os.path.basename(sub_dir)
for extension in extensions:
file_glob = os.path.join(INPUT_DATA, dir_name, '*.' + extension)
file_list.extend(glob.glob(file_glob))
if not file_list: continue

label_name = dir_name.lower()

#初始化
training_images = []
testing_images = []
validation_images = []
for file_name in file_list:
base_name = os.path.basename(file_name)#这个只是取回去文件名,去掉其路径
#basename的作用是去掉目录的路径,只返回文件名,而dirname用于 去掉文件名,只返回目录所在的路径。os.split()的作用是返回路径名和文件名的元组



# 随机划分数据
chance = np.random.randint(100)
if chance < validation_percentage:
validation_images.append(base_name)
elif chance < (testing_percentage + validation_percentage):
testing_images.append(base_name)
else:
training_images.append(base_name)

result[label_name] = {
'dir': dir_name,
'training': training_images,
'testing': testing_images,
'validation': validation_images,
}
return result


def get_image_path(image_lists, image_dir, label_name, index, category):
label_lists = image_lists[label_name]
category_list = label_lists[category]
mod_index = index % len(category_list)
base_name = category_list[mod_index]
sub_dir = label_lists['dir']
full_path = os.path.join(image_dir, sub_dir, base_name)
return full_path


def get_bottleneck_path(image_lists, label_name, index, category):
return get_image_path(image_lists, CACHE_DIR, label_name, index, category) + '.txt'


def run_bottleneck_on_image(sess, image_data, image_data_tensor, bottleneck_tensor):

bottleneck_values = sess.run(bottleneck_tensor, {image_data_tensor: image_data})

bottleneck_values = np.squeeze(bottleneck_values)
return bottleneck_values


def get_or_create_bottleneck(sess, image_lists, label_name, index, category, jpeg_data_tensor, bottleneck_tensor):
label_lists = image_lists[label_name]
sub_dir = label_lists['dir']
sub_dir_path = os.path.join(CACHE_DIR, sub_dir)
if not os.path.exists(sub_dir_path): os.makedirs(sub_dir_path)
bottleneck_path = get_bottleneck_path(image_lists, label_name, index, category)
if not os.path.exists(bottleneck_path):

image_path = get_image_path(image_lists, INPUT_DATA, label_name, index, category)

image_data = gfile.FastGFile(image_path, 'rb').read()

bottleneck_values = run_bottleneck_on_image(sess, image_data, jpeg_data_tensor, bottleneck_tensor)

bottleneck_string = ','.join(str(x) for x in bottleneck_values)
with open(bottleneck_path, 'w') as bottleneck_file:
bottleneck_file.write(bottleneck_string)
else:

with open(bottleneck_path, 'r') as bottleneck_file:
bottleneck_string = bottleneck_file.read()
bottleneck_values = [float(x) for x in bottleneck_string.split(',')]

return bottleneck_values


def get_random_cached_bottlenecks(sess, n_classes, image_lists, how_many, category, jpeg_data_tensor, bottleneck_tensor):
bottlenecks = []
ground_truths = []
for _ in range(how_many):
label_index = random.randrange(n_classes)
label_name = list(image_lists.keys())[label_index]
image_index = random.randrange(65536)
bottleneck = get_or_create_bottleneck(
sess, image_lists, label_name, image_index, category, jpeg_data_tensor, bottleneck_tensor)
ground_truth = np.zeros(n_classes, dtype=np.float32)
ground_truth[label_index] = 1.0
bottlenecks.append(bottleneck)
ground_truths.append(ground_truth)

return bottlenecks, ground_truths


def get_test_bottlenecks(sess, image_lists, n_classes, jpeg_data_tensor, bottleneck_tensor):
bottlenecks = []
ground_truths = []
label_name_list = list(image_lists.keys())
for label_index, label_name in enumerate(label_name_list):
category = 'testing'
for index, unused_base_name in enumerate(image_lists[label_name][category]):
bottleneck = get_or_create_bottleneck(sess, image_lists, label_name, index, category,jpeg_data_tensor, bottleneck_tensor)
ground_truth = np.zeros(n_classes, dtype=np.float32)
ground_truth[label_index] = 1.0
bottlenecks.append(bottleneck)
ground_truths.append(ground_truth)
return bottlenecks, ground_truths


def main():
image_lists = create_image_lists(TEST_PERCENTAGE, VALIDATION_PERCENTAGE)
n_classes = len(image_lists.keys())

# 读取已经训练好的Inception-v3的模型
with gfile.FastGFile(os.path.join(MODEL_DIR, MODEL_FILE), 'rb') as f:
graph_def = tf.GraphDef()
graph_def.ParseFromString(f.read())
bottleneck_tensor, jpeg_data_tensor = tf.import_graph_def(
graph_def, return_elements=[BOTTLENECK_TENSOR_NAME, JPEG_DATA_TENSOR_NAME])

# 定义新的神经网络输入
bottleneck_input = tf.placeholder(tf.float32, [None, BOTTLENECK_TENSOR_SIZE], name='BottleneckInputPlaceholder')
ground_truth_input = tf.placeholder(tf.float32, [None, n_classes], name='GroundTruthInput')

# 定义一个权链接层
with tf.name_scope('final_training_ops'):
weights = tf.Variable(tf.truncated_normal([BOTTLENECK_TENSOR_SIZE, n_classes], stddev=0.001))
biases = tf.Variable(tf.zeros([n_classes]))
logits = tf.matmul(bottleneck_input, weights) + biases
final_tensor = tf.nn.softmax(logits)

# 定义交叉商损失函数
cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits=logits, labels=ground_truth_input)
cross_entropy_mean = tf.reduce_mean(cross_entropy)
train_step = tf.train.GradientDescentOptimizer(LEARNING_RATE).minimize(cross_entropy_mean)

# 计算准确率
with tf.name_scope('evaluation'):
correct_prediction = tf.equal(tf.argmax(final_tensor, 1), tf.argmax(ground_truth_input, 1))
#tf.argmax()的用法,tf.argmax(final_tensor,1)返回的是final_tensor中,值最大的一个值,1在这里是指的返回一个值
#final_tensor返回的是概率的大小,返回的是概率的最大值作为最后的值
#tf.equal()的用法是比较tf.argmax(final_tensor, 1)和 tf.argmax(ground_truth_input, 1)对应位置的值是否相等,
#相等的时候返回true,否则返回false,然后统计true多占的比列就是最后的准确率。
evaluation_step = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
#tf.cast()的作用是改变数据类型的,改变correct_prediction的数据类型为float32
#tf.reduce_mean()的用法,在tensor的某一维度上,计算元素的平均值,由于输出的维度比原tensor降低了,所以也叫做降为。

with tf.Session() as sess:

sess.run(tf.global_variables_initializer())
# 训练过程
for i in range(STEPS):

train_bottlenecks, train_ground_truth = get_random_cached_bottlenecks(
sess, n_classes, image_lists, BATCH, 'training', jpeg_data_tensor, bottleneck_tensor)
sess.run(train_step,
feed_dict={bottleneck_input: train_bottlenecks, ground_truth_input: train_ground_truth})
#这里是train_bottlenecks,从磁盘读入的张量值作为输入向量,来训练全链接层,

if i % 100 == 0 or i + 1 == STEPS:
validation_bottlenecks, validation_ground_truth = get_random_cached_bottlenecks(
sess, n_classes, image_lists, BATCH, 'validation', jpeg_data_tensor, bottleneck_tensor)
validation_accuracy = sess.run(evaluation_step, feed_dict={
bottleneck_input: validation_bottlenecks, ground_truth_input: validation_ground_truth})
print('Step %d: Validation accuracy on random sampled %d examples = %.1f%%' %
(i, BATCH, validation_accuracy * 100))

# 在最后的测试数据上测试正确率
test_bottlenecks, test_ground_truth = get_test_bottlenecks(
sess, image_lists, n_classes, jpeg_data_tensor, bottleneck_tensor)
test_accuracy = sess.run(evaluation_step, feed_dict={
bottleneck_input: test_bottlenecks, ground_truth_input: test_ground_truth})
print('Final test accuracy = %.1f%%' % (test_accuracy * 100))


if __name__ == '__main__':#防止导入的包中的内容,也就是import 后面的内容也被运行
main()
#python是脚本语言,不像编译语言一样,先将程序编译成二进制再运行 ,而是动态的逐行解释运行,也就是从脚本的第一行开始运行,没有统一的入口。
#一个python源码除了可以直接运行外,还可以最为模块,也就是库导入,不管是导入还是运行,最顶层的代码都会被运行,python用缩进来区分代码层次,而
#实际上在导入的时候,有一部分代码我们是不希望被运行的。
#if __name__ =='main'就相当于程序的入口,python本身并没有规定这莫写,这只是一种编程习惯,由于模块之间相互引用,不同模块可能有这样的定义
#,而入口程序只能有一个,到底那一额入口程序被选中,这就取决于 __name__的值。
#__name__可以清晰的反映一个模块在包中的层次,其实,所谓模块的在包中的层次。__name__是内置变量,用于表示当前模块的名字,同时还能反映一个
#包的结构,如果模块直接运行的,则代码被运行,如果模块被导入的,则代码不能运行。
꧁༺The༒End༻꧂